//Description: web文件管理器（支持多文件批量上传。文件下载，目录浏览）
//Author: Zhouhui
//Release: 2017-12-26

package main

import (
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

var curDir string = "."
var root string

func main() {
	var e error

	// 如果没指定目录，则用当前目录
	if len(os.Args) == 1 {
		root = "."
	} else {
		root = os.Args[1]
	}

	e = os.Chdir(root)
	if e != nil {
		log.Fatal(e)
	}
	root, _ = filepath.Abs(root)

	http.HandleFunc("/", fileManager)
	http.HandleFunc("/upload", upload)
	http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { return })
	log.Fatal(http.ListenAndServe(":80", nil))
}

// 将指定目录下的 "文件名-大小" 生成为html
func genHtml(dir string, w http.ResponseWriter) {
	ns := lsFiles(dir)
	tplt, e := template.New("temp0").Parse(htmlTemplate)
	if e != nil {
		log.Fatalf("Parse(): %v\n", e)
	}

	if e = tplt.Execute(w, ns); e != nil {
		log.Fatalf("Execute() :%v\n", e)
	}
}

//浏览目录，或下载常规文件(不允许软连接，套接字，管道文件下载)
func fileManager(w http.ResponseWriter, r *http.Request) {
	peerAddr := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")]  //peerAddr用来记录浏览器IP地址
	targetPath := filepath.Join("./", r.URL.Path)

	fi, e := os.Lstat(targetPath)
	if e != nil {
		http.NotFound(w, r)
		return
	}

	switch {
	case fi.IsDir():
		curDir = targetPath
		genHtml(curDir, w)
		log.Printf("%s view dir %q\n", peerAddr, filepath.Join(root, targetPath))
		return
	case fi.Mode().IsRegular():
		f, e := os.Open(targetPath)
		if e != nil {
			log.Println(e)
			return
		}
		defer f.Close()
		_, e = io.Copy(w, f)
		if e != nil {
			log.Println(e)
			return
		}
		log.Printf("%s download %q\n", peerAddr, filepath.Join(root, targetPath))
	default:
		fmt.Fprintf(w, "<h1>%q不是目录也不是常规文件</h1>", r.URL.Path)
	}
}

//获取目录/文件列表
func lsFiles(dir string) map[template.URL]int64 {
	var fi []os.FileInfo
	fi, e := ioutil.ReadDir(dir)
	if e != nil {
		return nil
	}

	ns := make(map[template.URL]int64)
	for _, f := range fi {
		if f.IsDir() {
			ns[template.URL(f.Name()+"/")] = f.Size()
		} else {
			ns[template.URL(f.Name())] = f.Size()
		}
	}

	return ns
}

//多文件批量上传
func upload(w http.ResponseWriter, r *http.Request) {
	r.ParseMultipartForm(64 << 20)  //64MB内存buffer 

	var uplFail, upSucc int
	var rename string
	for _, fileHeader := range r.MultipartForm.File["uploadfile"] {
		srcFile, err := fileHeader.Open()
		if err != nil {
			log.Println(err)
			return
		}

		fpath := filepath.Join(curDir, fileHeader.Filename)
		originPath := fpath
		for {
			_, err := os.Stat(fpath) 
			if err == nil {
		        //如果文件存在，则重命名上传文件(.扩展名前加"_New")
				suffix := filepath.Ext(fpath)
				withoutSuf := strings.TrimSuffix(fpath, suffix)
				fpath = withoutSuf + "_New" + suffix
			} else {
				break
			}
		}
		if originPath != fpath {
			rename = rename + fmt.Sprintf("%-s&nbsp&nbsp<b>上传后被重命名为:</b>&nbsp&nbsp%-s </br>", originPath, fpath)
		}

		dstFile, err := os.Create(fpath)  //创建上传文件
		if err != nil {
			log.Println(err)
			srcFile.Close()
			return
		}

		_, err = io.Copy(dstFile, srcFile)  
		if err != nil {
			uplFail++
		} else {
			log.Printf("%s upload %q success\n", r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")], filepath.Join(root, dstFile.Name()))
			upSucc++
		}
		srcFile.Close() //这里是循环，避免用defer *.Close()
		dstFile.Close()
	}
	fmt.Fprintf(w, "<h1>上传失败:%d, 成功:%d</h1> <p>%s</p> <script language='javascript' type='text/javascript'> setTimeout(\"javascript:location.href='%s'\", %d000); </script>", uplFail, upSucc, rename, curDir, 1+len(strings.Split(rename, "</br>")))
}

const htmlTemplate = `
<html>
<head>
    <title>WEB文件管理</title>
    <style type="text/css">
        table td { width:100%; border-bottom:dotted 1px red; }
        a{text-decoration:none}
    </style>
</head>

<body>
    <form enctype="multipart/form-data" action="/upload" method="post">
        <input type="file" multiple name="uploadfile"/>
        <input type="submit" value="上传" />
    </form>

    <a style="font-weight:700; text-decoration: underline" href='/'>&#8634; 返回根目录</a></br>
    <a style="font-weight:700; text-decoration: underline" href='../'>&#8634; 返回上层目录</a>
    <table>
        <tr>
           <td style="font-weight:600">文件/目录名</td>
           <td style="font-weight:600">大小</h3></td>
        </tr>
    {{range $k,$v := .}}
        <tr>
           <td><a href='{{$k}}'>&bull; {{$k}}</a></td>
           <td>{{$v}}</td>
        </tr>
    {{end}}
    </table>
</body>
</html>
`
